﻿#region Licence
// <copyright file="Database.DAL/GenericProvider.cs" company="Kopigi">
// Copyright © Kopigi 2014
// </copyright>
// ****************************************************************************
// <author>Marc PLESSIS</author>
// <date>11/10/2014</date>
// <project>Nentang.wpf</project>
// <web>http://nentang.codeplex.com</web>
// <license>
// The MIT License (MIT)
// 
// Copyright (c) 2013 Marc Plessis
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// 
// </license>
#endregion
#region using

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core;
using System.Data.Entity.Core.Objects;
using System.Linq;
using System.Linq.Expressions;
using Database.POCO.Base;

#endregion

namespace Database.DAL
{
    /// <summary>
    /// Provider pour la base de données
    /// </summary>
    /// <typeparam name="T">Type de l'entité à utiliser</typeparam>
    public class GenericProvider<T> : IDisposable where T : BaseEntity
    {
        private const bool UseSingleInstanceOfDatabase = true;

        /// <summary>
        /// Context
        /// </summary>
        private DatabaseContext _context;

        /// <summary>
        /// The IObjectSet that represents the current entity.
        /// </summary>
        private readonly IObjectSet<T> _objectSet;

        /// <summary>
        /// Provider pour la base de données
        /// </summary>
        /// <param name="isOffline"></param>
        /// <param name="isNew"></param>
        /// <param name="withProxy"></param>
        public GenericProvider(bool isOffline, bool isNew = false, bool withProxy = true)
        {
            if (UseSingleInstanceOfDatabase && !isNew)
            {
                _context = null;
                GC.Collect();
                _context = DatabaseContext.GetInstance(isOffline);
            }
            else
            {
                _context = new DatabaseContext(isOffline);
            }

            _context.ObjectContext().ContextOptions.ProxyCreationEnabled = withProxy;
            _context.ObjectContext().ContextOptions.LazyLoadingEnabled = withProxy;
            _objectSet = _context.ObjectContext().CreateObjectSet<T>();            
        }

        /// <summary>
        /// Retourne une valeur indiquant si des enregistrements sont présents
        /// </summary>
        /// <returns>Indique si l'<see cref="ObjectSet{TEntity}" /> contient des données</returns>
        public bool Any()
        {
            return _objectSet.Any();
        }

        /// <summary>
        /// Retourne une valeur indiquant si des enregistrements sont présents, répondant aux critéres souhaités
        /// </summary>
        /// <param name="predicate"></param>
        /// <returns>Indique si l'<see cref="ObjectSet{TEntity}" /> contient des données</returns>
        public bool Any(Expression<Func<T, bool>> predicate)
        {
            return _objectSet.Any(predicate);
        }

        /// <summary>
        /// Ajoute un élément dans la liste d'objets. L'enregistrement n'est pas automatiquement lancé
        /// </summary>
        /// <param name="entity">Entité à ajouter</param>
        public void Add(T entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            _objectSet.AddObject(entity);    
        }

        /// <summary>
        /// Attache un élément dans la liste d'objets. L'enregistrement n'est pas automatiquement lancé
        /// </summary>
        /// <param name="entity">Entité à ajouter</param>
        public void Attach(T entity)
        {
            _objectSet.Attach(entity);
        }

        /// <summary>
        /// Renvoi le nombre d'objets présents dans la liste
        /// </summary>
        /// <returns>Nombre d'élément dans l'<see cref="ObjectSet{TEntity}"/></returns>
        public Int64 Count()
        {
            return _objectSet.Count();
        }

        /// <summary>
        /// Supprime un enregistrement
        /// </summary>
        /// <param name="entity">Entité à supprimer</param>
        public void Delete(T entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            if (!IsNew(entity))
            {
                _objectSet.DeleteObject(entity);    
            }
        }

        /// <summary>
        /// Supprime un ensemble d'enregistrement répondant aux critéres souhaités. L'enregistrement n'est pas automatiquement lancé
        /// </summary>
        /// <param name="predicate">Prédicat de recherche</param>
        public void Delete(Expression<Func<T, bool>> predicate)
        {
            IEnumerable<T> records = from x in _objectSet.Where<T>(predicate) select x;
            foreach (T record in records)
            {
                _objectSet.DeleteObject(record);
            }
        }

        /// <summary>
        /// Supprime tous les éléments de la liste d'enregistrement
        /// </summary>
        public void DeleteAll()
        {
            foreach (T record in _objectSet)
            {
                _objectSet.DeleteObject(record);
            }
        }

        /// <summary>
        /// Détection des changements éventuels
        /// </summary>
        public void DetectChanges()
        {
            _context.ObjectContext().DetectChanges();
        }

        /// <summary>
        /// Exécute une réquête qui ne renvoie pas de résultats, définie dans un String
        /// </summary>
        /// <param name="query"></param>
        public void ExecuteNonQuery(string query)
        {
            _context.Database.SqlQuery<T>(query);
        }

        /// <summary>
        /// Retourne l'objet contenant les éléments
        /// </summary>
        /// <returns>Liste des éléments contenus dans l'<see cref="ObjectSet{TEntity}"/> au format <see cref="IQueryable{T}"/></returns>
        public IQueryable<T> Fetch()
        {
            return _objectSet;
        }

        /// <summary>
        /// Retourne le premier élément d'une liste, répondant aux critéres souhaités
        /// </summary>
        /// <param name="predicate">Prédicat de recherche</param>
        /// <returns>Element trouvé</returns>
        public T First(Expression<Func<T, bool>> predicate)
        {
            return _objectSet.First<T>(predicate);
        }

        /// <summary>
        /// Retourne tous les éléments de la liste d'enregistrements
        /// </summary>
        /// <returns><see cref="IQueryable{T}"/> de tous les objets</returns>
        public IQueryable<T> GetAll()
        {
            try
            {
                return _objectSet;
            }
            catch (Exception)
            {
                return null;
            }
        }

        /// <summary>
        /// Retourne l'enregistrement correspondant à l'identifiant souhaité
        /// </summary>
        /// <param name="id">Identifiant de l'entité recherché</param>
        /// <returns>Element correspondant à l'identifiant</returns>
        public T GetById(Int64 id)
        {
            try
            {
                var result = _objectSet.Where(o => o.ID == id);
                if (result.Any())
                {
                    return result.First();
                }
                return null;
            }
            catch (ObjectNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Permet de savoir si l'enregistrement est nouveau dans la liste
        /// </summary>
        /// <param name="entity">Entité à tester</param>
        /// <returns>Indique si l'entité est nouvelle ou non pour l'<see cref="ObjectSet{TEntity}"/></returns>
        public bool IsNew(T entity)
        {
            ObjectStateEntry objectStateEntry = null;
            if (_context.ObjectContext().ObjectStateManager.TryGetObjectStateEntry(entity, out objectStateEntry))
            {
                return objectStateEntry.State == EntityState.Added;
            }
            return true;
        }

        /// <summary>
        /// Permet d'enregistrer un objet. Par défaut l'enregistrement n'est pas acté en base
        /// </summary>
        /// <param name="entity">Entité à sauvegarder</param>
        /// <param name="saveChanges">Indique si il faut sauvegarder immédiatement les changements en base de données</param>
        /// <param name="detectChanges">Indique si il faut recherche les changements éventuels dans l'<see cref="ObjectSet{TEntity}"/></param>
        /// <returns>Entité sauevgardée</returns>
        public T Save(T entity, bool saveChanges = true, bool detectChanges = true)
        {
            if (detectChanges)
            {
                _context.ObjectContext().DetectChanges();
            }

            if (this.IsNew(entity))
            {
                Add(entity);
            }
            else
            {
                if (_context.ObjectContext().ObjectStateManager.GetObjectStateEntry(entity).State != EntityState.Detached)
                {
                    _context.ObjectContext().ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
                }
                else
                {
                    Attach(entity);
                }
            }

            if (saveChanges)
            {
                _context.ObjectContext().SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
            }
            return entity;
        }

        /// <summary>
        /// Enregistre les changement en base de données
        /// </summary>
        /// <returns>Le nombre d'entité sauvegardées</returns>
        public int SaveChanges()
        {
            try
            {
                return _context.ObjectContext().SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
            }
            catch (Exception e)
            {
               throw new DatabaseSaveException(e);
            }
        }

        /// <summary>
        /// Renvoie le seul enregistrement qui correspond aux critéres souhaités
        /// </summary>
        /// <param name="predicate">Prédicat de recherche</param>
        /// <returns>Element SEUL trouvé dans l'<see cref="ObjectSet{TEntity}"/></returns>
        public T Single(Expression<Func<T, bool>> predicate)
        {
            if (!_objectSet.Any()) return null;
            return _objectSet.Single<T>(predicate);
        }

        /// <summary>
        /// Renvoie la liste des enregistrements qui correspondent aux critéres souhaités
        /// </summary>
        /// <param name="predicate">Prédicat de recherche</param>
        /// <returns>Liste au format <see cref="IQueryable{T}"/> des éléments trouvés</returns>
        public IQueryable<T> Where(Expression<Func<T, bool>> predicate)
        {
            return _objectSet.Where<T>(predicate);
        }

        #region Disposable

        public void Dispose()
        {
            Dispose(true);
        }

        #endregion

        #region protected

        protected virtual void Dispose(bool disposing)
        {
            if (_context != null)
            {
                _context.Dispose();
                _context = null;
            }
        }

        #endregion

    }
}
